﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;

namespace Luminal
{
    public sealed class PlatformIdentifier : LuminalObject
    {
        #region Interop
        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetPlatformIDs(uint num_entries,
            IntPtr[] platforms,
            out uint num_platforms);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetPlatformInfo(IntPtr platform,
            cl_platform_info param_name,
            SizeT param_value_size,
            IntPtr param_value,
            out SizeT param_value_size_ret);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetDeviceIDs(IntPtr platform,
            DeviceType device_type,
            uint num_entries,
            IntPtr[] devices,
            out uint num_devices);
        #endregion

        #region Static Initializer
        public static ReadOnlyCollection<PlatformIdentifier> AvailablePlatforms { get; private set; }

        public static PlatformIdentifier Default { get { return AvailablePlatforms.First(); } }

        static PlatformIdentifier()
        {
            AppDomain.CurrentDomain.ProcessExit += new EventHandler((sender, e) =>
            {
                foreach (PlatformIdentifier platform in AvailablePlatforms)
                    if (!platform.Disposed)
                        platform.Dispose();
            });
            
            uint num_platforms = 0;
            Util.ThrowError(
                "Could not get platform identifiers.",
                clGetPlatformIDs(0, null, out num_platforms));

            IntPtr[] platformPtrs = new IntPtr[num_platforms];
            Util.ThrowError(
                "Could not get platform identifiers.",
                clGetPlatformIDs(num_platforms, platformPtrs, out num_platforms));

            PlatformIdentifier[] platforms = new PlatformIdentifier[num_platforms];
            for (int i = 0; i < num_platforms; i++)
                platforms[i] = new PlatformIdentifier(platformPtrs[i]);

            AvailablePlatforms = platforms.ToList().AsReadOnly();
        }
        #endregion

        #region Properties
        public string Profile { get; private set; }
        public string Version { get; private set; }
        public string Name { get; private set; }
        public string Vendor { get; private set; }
        public ReadOnlyCollection<string> Extensions { get; private set; }

        private string GetInfo(cl_platform_info info)
        {
            SizeT infoSize;
            Util.ThrowError(
                "Could not get platform property.",
                clGetPlatformInfo(InternalPointer, info, 0, IntPtr.Zero, out infoSize));

            string output = new string(new char[infoSize - 1]); // minus 1 to account for null terminator
            Util.Pin(ref output, ptr =>
            {
                Util.ThrowError(
                    "Could not get platform property.",
                    clGetPlatformInfo(InternalPointer, info, infoSize, ptr, out infoSize));
            });

            return output;
        }
        #endregion

        #region Constructor
        private PlatformIdentifier(IntPtr ptr)
        {
            Initialize(ptr);

            try
            {
                // I don't expect that these properties would ever change, so preload them.
                Profile = GetInfo(cl_platform_info.CL_PLATFORM_PROFILE);
                Version = GetInfo(cl_platform_info.CL_PLATFORM_VERSION);
                Name = GetInfo(cl_platform_info.CL_PLATFORM_NAME);
                Vendor = GetInfo(cl_platform_info.CL_PLATFORM_VENDOR);
                Extensions = GetInfo(cl_platform_info.CL_PLATFORM_EXTENSIONS).Trim().Split(' ').ToList().AsReadOnly();
            }
            catch (Exception ex)
            {
                Dispose();
                throw ex;
            }
        }
        #endregion

        #region Methods
        public DeviceIdentifier[] GetDevices(DeviceType type)
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);

            uint deviceIDCount = 0;
            Util.ThrowError(
                "Could not get device identifiers.",
                clGetDeviceIDs(InternalPointer, type, 0, null, out deviceIDCount), 
                new ErrorCase(ErrorCode.CL_INVALID_DEVICE_TYPE, new ArgumentException("The specified DeviceType is not valid.", "type")),
                new ErrorCase(ErrorCode.CL_DEVICE_NOT_FOUND, new DeviceNotFoundException("No devices could be found for the specified DeviceType.")));

            IntPtr[] deviceIDPtrs = new IntPtr[deviceIDCount];
            Util.ThrowError(
                "Could not get device identifiers.",
                clGetDeviceIDs(InternalPointer, type, deviceIDCount, deviceIDPtrs, out deviceIDCount),
                new ErrorCase(ErrorCode.CL_INVALID_DEVICE_TYPE, new ArgumentException("The specified DeviceType is not valid.", "type")),
                new ErrorCase(ErrorCode.CL_DEVICE_NOT_FOUND, new DeviceNotFoundException("No devices could be found for the specified DeviceType.")));

            DeviceIdentifier[] output = new DeviceIdentifier[deviceIDCount];
            for (int i = 0; i < output.Length; i++)
                output[i] = DeviceIdentifier.FromPointer(deviceIDPtrs[i]);

            return output;
        }
        #endregion

        public static PlatformIdentifier FromPointer(IntPtr ptr)
        {
            if (ObjectTracker.ContainsObject(ptr))
                return ObjectTracker.GetObject<PlatformIdentifier>(ptr);
            else
                return new PlatformIdentifier(ptr);
        }
    }
}
